package com.idanchuang.component.gauss.aspect;

import com.idanchuang.component.base.exception.common.ErrorCode;
import com.idanchuang.component.base.exception.core.ExFactory;
import com.idanchuang.component.gauss.annotation.Idempotent;
import com.idanchuang.component.gauss.service.IdempotentService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;

/**
 * @Author: chac
 * @PackageName: com.idanchuang.component.gauss.aspect
 * @ClassName: IdempotentAspect
 * @Description:
 * @Date: 2021/2/5 14:18
 */
@Aspect
public class IdempotentAspect {

    private SpelExpressionParser parser = new SpelExpressionParser();

    private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    private IdempotentService idempotentService;

    public IdempotentAspect(IdempotentService idempotentService) {
        this.idempotentService = idempotentService;
    }

    @Pointcut("@annotation(com.idanchuang.component.gauss.annotation.Idempotent)")
    public void idemponentAnnotationPointcut() {
    }

    @Around("idemponentAnnotationPointcut()")
    public Object invokeWithIdemponent(ProceedingJoinPoint pjp) throws Throwable {
        Method method = resolveMethod(pjp);
        Idempotent annotation = method.getAnnotation(Idempotent.class);
        if (annotation == null) {
            throw new IllegalStateException("Wrong state for Idempotent annotation");
        }
        String key = generateKeySpEL(getSpelKey(annotation), pjp);
        Long expireTime = getExpire(annotation);

        if (idempotentService.checkIdempotent(key, expireTime)) {
            return pjp.proceed();
        }
        throw ExFactory.throwWith(ErrorCode.REPEAT_REQUEST, key + " :重复请求");
    }

    private Long getExpire(Idempotent idempotent) {
        Long expireTime = idempotent.expireTime();
        if (expireTime <= 0) {
            throw new IllegalStateException("expireTime is illegal");
        }
        return expireTime;
    }

    private String getSpelKey(Idempotent idempotent) {
        String spelKey = idempotent.spelKey();
        if ("".equals(spelKey)) {
            throw new IllegalStateException("spelKey is black");
        }
        return spelKey;
    }

    private Method resolveMethod(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Class<?> targetClass = joinPoint.getTarget().getClass();

        Method method = getDeclaredMethodFor(targetClass, signature.getName(),
                signature.getMethod().getParameterTypes());
        if (method == null) {
            throw new IllegalStateException("Cannot resolve target method: " + signature.getMethod().getName());
        }
        return method;
    }

    /**
     * Get declared method with provided name and parameterTypes in given class and its super classes.
     * All parameters should be valid.
     *
     * @param clazz          class where the method is located
     * @param name           method name
     * @param parameterTypes method parameter type list
     * @return resolved method, null if not found
     */
    private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
        try {
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                return getDeclaredMethodFor(superClass, name, parameterTypes);
            }
        }
        return null;
    }

    /**
     * 通过spel表达式获取参数
     * @param spELString
     * @param pjp
     * @return
     */
    private String generateKeySpEL(String spELString, ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String[] paramNames = nameDiscoverer.getParameterNames(method);
        Expression expression = parser.parseExpression(spELString);
        EvaluationContext context = new StandardEvaluationContext();

        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (spELString.equals(paramNames[i])) {
                return (String) args[i];
            }
            context.setVariable(paramNames[i], args[i]);
        }
        return expression.getValue(context).toString();
    }

}
